Skip to main content

Mobile login verification with deep links

Supacharger supports verified HTTPS links for mobile authentication. iOS calls these Universal Links; Android calls them App Links. The same email URL opens the installed app when the platform association succeeds and falls back to the web route when the app is unavailable.

This guide covers magic-link sign-in, link-based email confirmation, and password-reset callbacks. An email OTP is different: the user copies or autofills the configured-length code into the Supacharger form, so that flow does not need to leave and reopen the app.

How the callback works

app → Supabase Auth → email client

installed app ← verified HTTPS callback → browser fallback

same initiating client exchanges the one-use PKCE code

Supacharger's /auth/callback Route Handler exchanges the code for a session. Supabase PKCE codes expire quickly, can be exchanged only once, and require the verifier stored by the client that started the flow. Therefore:

  • a wrapped Supacharger web app must load the incoming URL in the same persistent web view and cookie store that requested the email;
  • a fully native app must start and complete the flow with the same native Supabase client and secure storage adapter; and
  • neither implementation should copy access tokens, refresh tokens, or PKCE verifiers into logs, analytics, application metadata, or another URL.

See Supabase's PKCE flow, native mobile deep linking, and redirect URL guidance.

Configure Supacharger

Edit the developer-owned src/supacharger.config.ts (supacharger.config.ts in Specdrive). Replace every example value with identifiers belonging to the application being signed:

MOBILE_DEEP_LINKING: {
ENABLED: true,
ASSOCIATED_PATHS: [
'/auth/callback',
'/auth/confirm',
],
IOS: {
APP_IDS: ['ABCDE12345.com.example.myapp'],
},
ANDROID: {
APPS: [
{
PACKAGE_NAME: 'com.example.myapp',
SHA256_CERT_FINGERPRINTS: [
'<PLAY_APP_SIGNING_SHA256_FINGERPRINT>',
],
},
],
},
},

IOS.APP_IDS uses <Apple App ID prefix>.<bundle identifier>, not the numeric App Store ID. ANDROID.APPS can contain separate debug, staging, or production packages. A package can contain multiple fingerprints during an intentional signing-key transition.

The example Apple prefix, bundle identifier, Android package, domain, and fingerprint are placeholders. Public documentation must never reproduce an application's real values merely to provide an example.

When a platform has no configured identifiers, its well-known endpoint returns 404. Once enabled and configured, Supacharger generates current platform documents at:

https://app.example.com/.well-known/apple-app-site-association
https://app.example.com/.well-known/assetlinks.json

Each URL must respond directly over HTTPS with status 200, Content-Type: application/json, and no redirect. Configure every production subdomain independently. The generated iOS document uses Apple's current appIDs and components format; the Android document uses delegate_permission/common.handle_all_urls.

Configure Supabase Auth

In each hosted Supabase environment:

  1. Set Authentication → URL Configuration → Site URL to the canonical production web origin, such as https://app.example.com.

  2. Add exact production redirect URLs for every enabled flow:

    https://app.example.com/auth/callback
    https://app.example.com/auth/confirm
  3. Use broad /** patterns only for local development or deployment previews. Prefer exact production paths.

  4. If an email template constructs its own confirmation URL while the application supplies emailRedirectTo, use Supabase's {{ .RedirectTo }} variable as documented. Keep {{ .Token }} for the OTP template.

  5. Configure hosted settings separately from supabase/config.toml; the repository file affects only the local stack.

Supacharger's magic-link operation sends users to /auth/callback. Password recovery uses /auth/callback?flow=recovery, so it is covered by the same callback path and does not associate the internal /account/reset-password/new page. Link-based sign-up confirmation uses /auth/confirm with token_hash and type. OTP sign-up calls verifyOtp with the entered email and token and does not use the well-known endpoints.

Follow Apple's associated domains and Universal Link guidance:

  1. In Xcode, select the native target and add Signing & Capabilities → Associated Domains.

  2. Add the exact host without a scheme, path, query, or trailing slash:

    applinks:app.example.com
  3. Confirm that the signed target's application identifier exactly matches an IOS.APP_IDS entry.

  4. Accept only the expected HTTPS host and paths when continuing the user activity. A web-wrapper bridge can use this shape:

    .onContinueUserActivity(NSUserActivityTypeBrowsingWeb) { activity in
    guard let url = activity.webpageURL,
    url.scheme == "https",
    url.host == "app.example.com",
    ["/auth/callback", "/auth/confirm"]
    .contains(url.path)
    else { return }

    authWebView.load(URLRequest(url: url))
    }

Use the web view and data store that initiated authentication. A native Swift client should instead give the verified URL to its native auth coordinator and complete the code exchange there.

Apple fetches the association through its CDN and may cache it. The file must be named apple-app-site-association without a .json extension. Apple's Universal Link diagnostics recommend:

sudo swcutil dl -d app.example.com
sudo swcutil verify -d app.example.com -j ./apple-app-site-association \
-u 'https://app.example.com/auth/callback'

On a device, paste the link into Notes and long-press it. Typing the URL directly into Safari's address bar intentionally remains browser navigation and is not a valid Universal Link test.

Follow Android's App Link intent-filter, website association, and verification guidance.

Use the application ID from the native module's Gradle configuration. When Google Play App Signing is enabled, use the app-signing certificate fingerprint shown by Play Console—not the local upload-key fingerprint. Fingerprints are uppercase, colon-separated SHA-256 values.

Declare verified HTTPS paths in AndroidManifest.xml. Separate filters avoid accidental combinations when hosts or path rules later diverge:

<activity
android:name=".MainActivity"
android:exported="true">

<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="https"
android:host="app.example.com"
android:pathPrefix="/auth/" />
</intent-filter>
</activity>

The generated assetlinks.json proves the package/domain relationship. On Android versions before dynamic App Links, the native manifest remains responsible for path restrictions, so keep it aligned with ASSOCIATED_PATHS.

Handle both a cold start and a new intent, then validate the URL again before loading or exchanging anything:

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
handleAuthLink(intent)
}

override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
handleAuthLink(intent)
}

private fun handleAuthLink(intent: Intent) {
val uri = intent.data ?: return
val allowedPaths = setOf(
"/auth/callback",
"/auth/confirm",
)

if (uri.scheme != "https" ||
uri.host != "app.example.com" ||
uri.path !in allowedPaths
) return

authWebView.loadUrl(uri.toString())
}

A fully native Kotlin client should pass the validated URI to its native auth coordinator instead of a web view.

After installing the signed build, wait for verification and run:

adb shell pm set-app-links --package com.example.myapp 0 all
adb shell pm verify-app-links --re-verify com.example.myapp
adb shell pm get-app-links com.example.myapp
adb shell am start -W -a android.intent.action.VIEW \
-c android.intent.category.BROWSABLE \
-d 'https://app.example.com/auth/callback?code=test'

The host should report verified. A legacy_failure, browser chooser, or browser-only result usually means the deployed file redirected, the package did not match, the wrong signing certificate was used, or the manifest host/path differed.

Deployment and security checklist

  • Deploy the well-known routes before shipping a native build that declares the domain.
  • Use exact production hosts and callback paths; do not accept arbitrary next, host, scheme, or path values.
  • Preserve the full callback query string, but never log it. Auth codes and token hashes are short-lived credentials.
  • Keep the callback in the client that initiated PKCE. Do not attempt a second exchange after the code has been consumed.
  • Test installed and uninstalled behaviour. Without the app, the same HTTPS URL must complete safely in the browser.
  • Test cold start, warm start, expired links, cancelled sign-in, staging and production signing, and password reset separately.
  • Do not enable Android with a guessed package or fingerprint. Do not enable iOS with another application's App ID.
  • Re-test after changing domains, native application identifiers, signing certificates, callback paths, email templates, or Supabase redirect settings.
Build Supacharger with SpecdriveKeep the specification, infrastructure and agent work in one project context.